package geowiki.vis;

import geowiki.utils.MathUtils;
import processing.core.PApplet;
import processing.core.PVector;

public class GumConnection {

	private static final float DEFAULT_RADIUS = 40;
	
	/** The radius of the circles. */
	public float radiusSP;
	public float radiusEP;
	public float radius;
	
	/**
	 * The maximum distance between two circles. Used to normalize the vertical distance of the
	 * anchor points to the direct line between both centers.
	 */
	public float maxDistance = 500;

	/** The color of the connection. */
	public int col;

	/** Start point. The center of the start circle. */
	public PVector sp;

	/** End point. The center of the end circle. */
	public PVector ep;

	/** 1st upper anchor point. */
	PVector apTop1 = new PVector();
	/** 2nd upper anchor point. */
	PVector apTop2 = new PVector();
	/** 1st lower anchor point. */
	PVector apBot1 = new PVector();
	/** 2nd lower anchor point. */
	PVector apBot2 = new PVector();

	/** Perpendicular point at the end circle upper outline. */
	PVector epTop = new PVector();
	/** Perpendicular point at the start circle upper outline. */
	PVector spTop = new PVector();
	/** Perpendicular point at the end circle lower outline. */
	PVector epBot = new PVector();
	/** Perpendicular point at the start circle lower outline. */
	PVector spBot = new PVector();

	PApplet p;

	public boolean autoUpdate = true;

	private PVector apTop11;

	private PVector apTop12;

	private PVector apTopCenter;

	private PVector apTop21;

	private PVector apTop22;

	private PVector apBot11;

	private PVector apBot12;

	private PVector apBotCenter;

	private PVector apBot21;

	private PVector apBot22;

	public GumConnection(PApplet p, PVector sp, PVector ep, float radiusSP, float radiusEP) {
		this.p = p;
		this.sp = sp;
		this.ep = ep;
		// Set default color to half-transparent black
		this.col = p.color(0, 50);
		
		this.radiusSP = radiusSP;
		this.radiusEP = radiusEP;
		updateRadius();
	}
	
	public GumConnection(PApplet p, PVector sp, PVector ep) {
		this(p, sp, ep, DEFAULT_RADIUS, DEFAULT_RADIUS);
	}

	public GumConnection(PApplet p, PVector sp, PVector ep, boolean autoUpdate) {
		this(p, sp, ep);
		this.autoUpdate = autoUpdate;
	}
	
	public void setRadiusSP(float radiusSP) {
		this.radiusSP = radiusSP;
		updateRadius();
	}
	
	public void addRadiusSP(float radiusSPDiff) {
		this.radiusSP += radiusSPDiff;
		updateRadius();
	}
	
	private void updateRadius() {
		this.radius = (radiusSP + radiusEP) / 2;
	}

	public void draw() {
		if (autoUpdate) {
			updateAnchorPoints();
		}

		p.noStroke();
		p.fill(col);
		p.beginShape();

		// Start point top
		p.vertex(spTop.x, spTop.y);
		// Top Bezier curves from start to end
		p.bezierVertex(apTop11.x, apTop11.y, apTop12.x, apTop12.y, apTopCenter.x, apTopCenter.y);
		p.bezierVertex(apTop21.x, apTop21.y, apTop22.x, apTop22.y, epTop.x, epTop.y);
		// End circle (from end top to end bottom)
		drawCircleVertices(ep, sp, radiusEP);

		// End point bottom
		p.vertex(epBot.x, epBot.y);
		// Bottom Bezier curves from end to start
		p.bezierVertex(apBot22.x, apBot22.y, apBot21.x, apBot21.y, apBotCenter.x, apBotCenter.y);
		p.bezierVertex(apBot12.x, apBot12.y, apBot11.x, apBot11.y, spBot.x, spBot.y);
		// Start circle (from start bottom to start top)
		drawCircleVertices(sp, ep, radiusSP);

		p.endShape();
	}

	public void drawCircleVertices(PVector center, PVector center2, float radius) {
		float angle = MathUtils.getAngleBetween(center2, center);
		int circleVertexNr = 36;
		for (int i = 0; i < circleVertexNr; i++) {
			float theta = -angle - (PApplet.PI / circleVertexNr * i);
			float x = PApplet.sin(theta) * radius;
			float y = PApplet.cos(theta) * radius;
			p.vertex(center.x + x, center.y + y);

			/*
			 * p.noStroke(); p.fill(255 / circleVertexNr * i); p.ellipse(center.x + x, center.y + y,
			 * 6, 6); p.noFill();
			 */
		}
	}

	public void drawDebug() {
		// Show upper and lower perpendicular points
		p.fill(0, 255, 0, 100);
		p.ellipse(epTop.x, epTop.y, 10, 10);
		p.ellipse(spTop.x, spTop.y, 10, 10);
		p.fill(0, 255, 255, 100);
		p.ellipse(epBot.x, epBot.y, 10, 10);
		p.ellipse(spBot.x, spBot.y, 10, 10);

		// Show anchor points
		p.fill(255, 0, 0, 40);
		p.ellipse(apTop1.x, apTop1.y, 5, 5);
		p.ellipse(apTop2.x, apTop2.y, 5, 5);
		p.ellipse(apBot1.x, apBot1.y, 5, 5);
		p.ellipse(apBot2.x, apBot2.y, 5, 5);

		p.noFill();
	}

	public void updateAnchorPoints() {
		PVector pv = getPerpendicular(sp, ep);
		updatePerpendicularPoints(pv);

		float fract1 = 1 / 7f;
		float fract2 = 2 / 7f;
		float center = 1 / 2f;

		// Top
		// Calculate anchor points. Vertical distance depends on the horizontal distance.
		float ydistTop = PApplet.map(PVector.dist(spTop, epTop), 0, maxDistance, radius, 0);
		// Limit vertical distance so even for distances > maxDistance no overlapping appears.
		ydistTop = PApplet.max(ydistTop, radius * 0.1f);
		apTop11 = calcAnchorPoint(pv, radiusSP, fract1);
		apTop12 = calcAnchorPoint(pv, ydistTop, fract2);
		apTopCenter = calcAnchorPoint(pv, ydistTop, center);
		apTop21 = calcAnchorPoint(pv, ydistTop, 1 - fract2);
		apTop22 = calcAnchorPoint(pv, radiusEP, 1 - fract1);

		// Bottom
		float ydistBot = PApplet.map(PVector.dist(spBot, epBot), 0, maxDistance, -radius, 0);
		ydistBot = PApplet.min(ydistBot, -radius * 0.1f);
		apBot11 = calcAnchorPoint(pv, -radiusSP, fract1);
		apBot12 = calcAnchorPoint(pv, ydistBot, fract2);
		apBotCenter = calcAnchorPoint(pv, ydistBot, center);
		apBot21 = calcAnchorPoint(pv, ydistBot, 1 - fract2);
		apBot22 = calcAnchorPoint(pv, -radiusEP, 1 - fract1);
	}

	private PVector calcAnchorPoint(PVector pv, float ydist, float fract) {
		PVector ap = new PVector();
		ap.x = PApplet.lerp(sp.x + pv.x * ydist, ep.x + pv.x * ydist, fract);
		ap.y = PApplet.lerp(sp.y + pv.y * ydist, ep.y + pv.y * ydist, fract);
		return ap;
	}

	// Calculate perpendicular points of both circles
	protected void updatePerpendicularPoints(PVector pv) {
		epTop.x = ep.x + pv.x * radiusEP;
		epTop.y = ep.y + pv.y * radiusEP;
		spTop.x = sp.x + pv.x * radiusSP;
		spTop.y = sp.y + pv.y * radiusSP;

		epBot.x = ep.x - pv.x * radiusEP;
		epBot.y = ep.y - pv.y * radiusEP;
		spBot.x = sp.x - pv.x * radiusSP;
		spBot.y = sp.y - pv.y * radiusSP;
	}

	/**
	 * Returns the perpendicular vector to the line between the two given vectors.
	 * 
	 * @param sp
	 *            Start point.
	 * @param ep
	 *            End point.
	 * @return The perpendicular vector.
	 */
	protected PVector getPerpendicular(PVector sp, PVector ep) {
		float angle = MathUtils.getAngleBetween(sp, ep);
		float theta = PApplet.HALF_PI + angle;
		float x = PApplet.cos(theta);
		float y = PApplet.sin(theta);
		return new PVector(x, y);
	}

	public void setEndPoint(int x, int y) {
		ep.x = x;
		ep.y = y;
	}

	// Experiments --------------------------------------------------

	public void drawWithCircles() {
		if (autoUpdate) {
			updateAnchorPoints();
		}

		p.fill(col);

		// Draw start and end circles
		p.ellipse(sp.x, sp.y, radius * 2, radius * 2);
		p.ellipse(ep.x, ep.y, radius * 2, radius * 2);

		// Draw shape
		p.beginShape();
		p.vertex(spTop.x, spTop.y);
		p.bezierVertex(apTop1.x, apTop1.y, apTop2.x, apTop2.y, epTop.x, epTop.y);
		p.vertex(epBot.x, epBot.y);
		p.bezierVertex(apBot2.x, apBot2.y, apBot1.x, apBot1.y, spBot.x, spBot.y);
		p.vertex(spTop.x, spTop.y);
		p.endShape();
	}

	public void drawWithCircleVertices() {
		if (autoUpdate) {
			updateAnchorPoints();
		}

		// Draw shape
		p.beginShape();
		p.vertex(spTop.x, spTop.y);
		p.bezierVertex(apTop1.x, apTop1.y, apTop2.x, apTop2.y, epTop.x, epTop.y);

		// Draw end circle
		float angle = MathUtils.getAngleBetween(sp, ep);
		int circleVertexNr = 36;
		for (int i = 0; i < circleVertexNr; i++) {
			float theta = -angle - (PApplet.PI / circleVertexNr * i);
			float x = PApplet.sin(theta) * radius;
			float y = PApplet.cos(theta) * radius;

			p.noStroke();
			p.fill(255 / circleVertexNr * i);
			// p.ellipse(ep.x + x, ep.y + y, 6, 6);
			p.noFill();

			p.vertex(ep.x + x, ep.y + y);
		}
		p.vertex(epBot.x, epBot.y);

		p.bezierVertex(apBot2.x, apBot2.y, apBot1.x, apBot1.y, spBot.x, spBot.y);
		p.vertex(spTop.x, spTop.y);
		p.fill(col);
		p.endShape();
	}

	public void updateAnchorPointsSingleBezier() {
		PVector pv = getPerpendicular(sp, ep);
		updatePerpendicularPoints(pv);

		float fract1 = 1 / 5f;
		float fract2 = 1 - fract1;
		// Calculate anchor points. Vertical distance depends on the horizontal distance.
		float ydistTop = PApplet.map(PVector.dist(spTop, epTop), 0, maxDistance, radius, 0);
		// Limit vertical distance so even for distances > maxDistance no overlapping appears.
		ydistTop = PApplet.max(ydistTop, -radius * 0.3f);
		apTop1 = calcAnchorPoint(pv, ydistTop, fract1);
		apTop2 = calcAnchorPoint(pv, ydistTop, fract2);

		float ydistBot = PApplet.map(PVector.dist(spBot, epBot), 0, maxDistance, -radius, 0);
		ydistBot = PApplet.min(ydistBot, radius * 0.3f);
		apBot1 = calcAnchorPoint(pv, ydistBot, fract1);
		apBot2 = calcAnchorPoint(pv, ydistBot, fract2);
	}

}
